package com.remoter.context.spring;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.annotation.InjectionMetadata;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

import com.remoter.context.spring.annotation.Consumer;
import com.remoter.context.spring.bean.ConsumerBean;

public class RemoterAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware{
	
	private final Set<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>();
	private final Map<Class<?>,Constructor<?>[]> candidateConstructorsCache = new ConcurrentHashMap<Class<?>,Constructor<?>[]>(64);
	private final Map<Class<?>,InjectionMetadata> injectionMetadataCache = new ConcurrentHashMap<Class<?>,InjectionMetadata>(64);
	
	private int order = Ordered.LOWEST_PRECEDENCE - 2;
	private String requiredParameterName = "required";
	private boolean requiredParameterValue = true;
	private ConfigurableListableBeanFactory beanFactory;
	
	public RemoterAnnotationBeanPostProcessor(){
		this.autowiredAnnotationTypes.add(Consumer.class);
	}
	
	@Override
	public int getOrder() {
		return this.order;
	}

	@Override
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		if(!(beanFactory instanceof ConfigurableListableBeanFactory)){
			throw new IllegalArgumentException("AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory");
		}
		this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
	}

	@Override
	public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType,String beanName) {
		if(beanType != null){
			InjectionMetadata metadata = findAutowiringMetadata(beanType);
			metadata.checkConfigMembers(beanDefinition);
		}
	}
	
	@Override
	public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass,String beanName) throws BeansException{
		Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);
		if(candidateConstructors == null){
			synchronized(this.candidateConstructorsCache){
				candidateConstructors = this.candidateConstructorsCache.get(beanClass);
				if(candidateConstructors == null){
					Constructor<?>[] rawCandidates = beanClass.getDeclaredConstructors();
					List<Constructor<?>> candidates = new ArrayList<Constructor<?>>(rawCandidates.length);
					Constructor<?> requiredConstructor = null;
					Constructor<?> defaultConstructor = null;
					for(Constructor<?> candidate : rawCandidates){
						Annotation annotation = findAutowiredAnnotation(candidate);
						if(annotation != null){
							if(requiredConstructor != null){
								throw new BeanCreationException("Invalid autowire-marked constructor: " + candidate
										+ ". Found another constructor with 'required' Autowired annotation: "
										+ requiredConstructor);
							}
							if(candidate.getParameterTypes().length == 0){
								throw new IllegalStateException("Autowired annotation requires at least one argument: " + candidate);
							}
							boolean required = determineRequiredStatus(annotation);
							if(required){
								if(!candidates.isEmpty()){
									throw new BeanCreationException("Invalid autowire-marked constructors: " + candidates
													+ ". Found another constructor with 'required' Autowired annotation: "
													+ requiredConstructor);
								}
								requiredConstructor = candidate;
							}
							candidates.add(candidate);
						}else if(candidate.getParameterTypes().length == 0){
							defaultConstructor = candidate;
						}
					}
					if(!candidates.isEmpty()){
						if(requiredConstructor == null && defaultConstructor != null){
							candidates.add(defaultConstructor);
						}
						candidateConstructors = candidates.toArray(new Constructor[candidates.size()]);
					}else{
						candidateConstructors = new Constructor[0];
					}
					this.candidateConstructorsCache.put(beanClass, candidateConstructors);
				}
			}
		}
		return (candidateConstructors.length > 0 ? candidateConstructors : null);
	}
	
	@Override
	public PropertyValues postProcessPropertyValues(PropertyValues pvs,PropertyDescriptor[] pds, Object bean, String beanName)throws BeansException {
		InjectionMetadata metadata = findAutowiringMetadata(bean.getClass());
		try{
			metadata.inject(bean, beanName, pvs);
		}catch(Throwable e){
			throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", e);
		}
		return pvs;
	}

	public void setAutowiredAnnotationType(Class<? extends Annotation> autowiredAnnotationType){
		Assert.notNull(autowiredAnnotationType, "'autowiredAnnotationType' must not be null");
		this.autowiredAnnotationTypes.clear();
		this.autowiredAnnotationTypes.add(autowiredAnnotationType);
	}
	
	public void setAutowiredAnnotationTypes(Set<Class<? extends Annotation>> autowiredAnnotationTypes){
		Assert.notEmpty(autowiredAnnotationTypes, "'autowiredAnnotationTypes' must not be empty");
		this.autowiredAnnotationTypes.clear();
		this.autowiredAnnotationTypes.addAll(autowiredAnnotationTypes);
	}
	
	public void setRequiredParameterName(String requiredParameterName){
		this.requiredParameterName = requiredParameterName;
	}
	
	public void setRequiredParameterValue(boolean requiredParameterValue){
		this.requiredParameterValue = requiredParameterValue;
	}
	
	public void setOrder(int order){
		this.order = order;
	}
	
	private InjectionMetadata findAutowiringMetadata(Class<?> clazz){
		InjectionMetadata metadata = this.injectionMetadataCache.get(clazz);
		if(null == metadata){
			synchronized(this.injectionMetadataCache){
				metadata = this.injectionMetadataCache.get(clazz);
				if(null == metadata){
					metadata = buildAutowiringMetadata(clazz);
					this.injectionMetadataCache.put(clazz, metadata);
				}
			}
		}
		return metadata;
	}
	
	private InjectionMetadata buildAutowiringMetadata(Class<?> clazz){
		LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<InjectionMetadata.InjectedElement>();
		Class<?> targetClass = clazz;
		do{
			LinkedList<InjectionMetadata.InjectedElement> currElements = new LinkedList<InjectionMetadata.InjectedElement>();
			for(Field field : targetClass.getDeclaredFields()){
				Annotation annotation = findAutowiredAnnotation(field);
				if(annotation != null){
					if(Modifier.isStatic(field.getModifiers())){
						continue;
					}
					boolean required = determineRequiredStatus(annotation);
					currElements.add(new AutowiredFieldElement(field,required));
				}
			}
			for(Method method : targetClass.getDeclaredMethods()){
				Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
				Annotation annotation = BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod) ? findAutowiredAnnotation(bridgedMethod) : findAutowiredAnnotation(method);
				if(annotation != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))){
					if(Modifier.isStatic(method.getModifiers())){
						continue;
					}
					boolean required = determineRequiredStatus(annotation);
					PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method);
					currElements.add(new AutowiredMethodElement(method, required, pd));
				}
			}
			elements.addAll(0, currElements);
			targetClass = targetClass.getSuperclass();
		}while(targetClass != null && targetClass != Object.class);
		return new InjectionMetadata(clazz,elements);
	}
	
	private Annotation findAutowiredAnnotation(AccessibleObject ao){
		for(Class<? extends Annotation> type : this.autowiredAnnotationTypes){
			Annotation annotation = AnnotationUtils.getAnnotation(ao,type);
			if(null != annotation){
				return annotation;
			}
		}
		return null;
	}
	
	protected <T> Map<String, T> findAutowireCandidates(Class<T> type) throws BeansException{
		if(this.beanFactory == null){
			throw new IllegalStateException("No BeanFactory configured - override the getBeanOfType method or specify the 'beanFactory' property");
		}
		return BeanFactoryUtils.beansOfTypeIncludingAncestors(this.beanFactory,type);
	}
	
	protected boolean determineRequiredStatus(Annotation annotation){
		try{
			Method method = ReflectionUtils.findMethod(annotation.annotationType(), this.requiredParameterName);
			if(null == method){
				return true;
			}
			return (this.requiredParameterValue == (Boolean) ReflectionUtils.invokeMethod(method, annotation));
		}catch(Exception e){
			return true;
		}
	}
	
	private void registerDependentBeans(String beanName, Set<String> autowiredBeanNames){
		if(null != beanName){
			for(String autowiredBeanName : autowiredBeanNames){
				if(this.beanFactory.containsBean(autowiredBeanName)){
					this.beanFactory.registerDependentBean(autowiredBeanName, beanName);
				}
			}
		}
	}
	
	private Object resolvedCachedArgument(String beanName,Object cachedArgument){
		if(cachedArgument instanceof DependencyDescriptor){
			DependencyDescriptor descriptor = (DependencyDescriptor) cachedArgument;
			TypeConverter typeConverter = this.beanFactory.getTypeConverter();
			return this.beanFactory.resolveDependency(descriptor, beanName, null, typeConverter);
		}else if(cachedArgument instanceof RuntimeBeanReference){
			return this.beanFactory.getBean(((RuntimeBeanReference)cachedArgument).getBeanName());
		}else{
			return cachedArgument;
		}
	}
	
	private class AutowiredFieldElement extends InjectionMetadata.InjectedElement{
		private final boolean required;
		private volatile boolean cached = false;
		private volatile Object cachedFieldValue;
		public AutowiredFieldElement(Field field, boolean required){
			super(field, null);
			this.required = required;
		}
		@Override
		protected void inject(Object target, String requestingBeanName,PropertyValues pvs) throws Throwable {
			Field field = (Field)this.member;
			Consumer consumer = field.getAnnotation(Consumer.class);
			Class<?> type = consumer.type();
			if(type.equals(Void.class)){
				type = field.getType();
			}
			DefaultListableBeanFactory factory = (DefaultListableBeanFactory) beanFactory;
			RootBeanDefinition beanDefinition = new RootBeanDefinition(ConsumerBean.class);
			beanDefinition.setLazyInit(false);
			beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0,consumer.name());
			beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(1,type);
			factory.registerBeanDefinition(field.getName(),beanDefinition);
			try{
				Object value;
				if(this.cached){
					value = resolvedCachedArgument(requestingBeanName,this.cachedFieldValue);
				}else{
					DependencyDescriptor descriptor = new DependencyDescriptor(field,this.required);
					Set<String> autowiredBeanNames = new LinkedHashSet<String>(1);
					TypeConverter typeConverter = beanFactory.getTypeConverter();
					value = beanFactory.resolveDependency(descriptor,requestingBeanName,autowiredBeanNames,typeConverter);
					synchronized(this){
						if(!this.cached){
							if(value != null || this.required){
								this.cachedFieldValue = descriptor;
								registerDependentBeans(requestingBeanName,autowiredBeanNames);
								if(autowiredBeanNames.size() == 1){
									String autowiredBeanName = autowiredBeanNames.iterator().next();
									if(beanFactory.containsBean(autowiredBeanName)){
										if(beanFactory.isTypeMatch(autowiredBeanName,field.getType())){
											this.cachedFieldValue = new RuntimeBeanReference(autowiredBeanName);
										}
									}
								}
							}else{
								this.cachedFieldValue = null;
							}
							this.cached = true;
						}
					}
				}
				if(null != value){
					ReflectionUtils.makeAccessible(field);
					field.set(target,value);
				}
			}catch(Exception e){
				throw new BeanCreationException("Could not autowire field: " + field,e);
			}
		}
	}
	
	private class AutowiredMethodElement extends InjectionMetadata.InjectedElement{
		private final boolean required;
		private volatile boolean cached = false;
		private volatile Object[] cachedMethodArguments;
		public AutowiredMethodElement(Method method, boolean required,PropertyDescriptor pd){
			super(method,pd);
			this.required = required;
		}
		@Override
		protected void inject(Object target, String requestingBeanName,PropertyValues pvs) throws Throwable {
			if(checkPropertySkipping(pvs)){
				return;
			}
			Method method = (Method)this.member;
			try{
				Object[] arguments;
				if(this.cached){
					arguments = this.resolveCachedArguments(requestingBeanName);
				}else{
					Class<?>[] paramTypes = method.getParameterTypes();
					arguments = new Object[paramTypes.length];
					DependencyDescriptor[] descriptors = new DependencyDescriptor[paramTypes.length];
					Set<String> autowiredBeanNames = new LinkedHashSet<String>(paramTypes.length);
					TypeConverter typeConverter = beanFactory.getTypeConverter();
					for(int i = 0; i < arguments.length; i++){
						MethodParameter methodParam = new MethodParameter(method, i);
						GenericTypeResolver.resolveParameterType(methodParam, target.getClass());
						descriptors[i] = new DependencyDescriptor(methodParam, this.required);
						arguments[i] = beanFactory.resolveDependency(descriptors[i], requestingBeanName, autowiredBeanNames,typeConverter);
						if(arguments[i] == null && !this.required){
							arguments = null;
							break;
						}
					}
					synchronized(this){
						if(!this.cached){
							if(arguments != null){
								this.cachedMethodArguments = new Object[arguments.length];
								for(int i = 0; i < arguments.length; i++){
									this.cachedMethodArguments[i] = descriptors[i];
								}
								registerDependentBeans(requestingBeanName, autowiredBeanNames);
								if(autowiredBeanNames.size() == paramTypes.length){
									Iterator<String> it = autowiredBeanNames.iterator();
									for(int i = 0; i < paramTypes.length; i++){
										String autowiredBeanName = it.next();
										if(beanFactory.containsBean(autowiredBeanName)){
											if(beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])){
												this.cachedMethodArguments[i] = new RuntimeBeanReference(autowiredBeanName);
											}
										}
									}
								}
							}else{
								this.cachedMethodArguments = null;
							}
							this.cached = true;
						}
					}
				}
				if(null != arguments){
					ReflectionUtils.makeAccessible(method);
					method.invoke(target,arguments);
				}
			}catch(InvocationTargetException ex){
				throw ex.getTargetException();
			}catch(Exception e){
				throw new BeanCreationException("Could not autowire method: " + method, e);
			}
		}
		
		private Object[] resolveCachedArguments(String beanName){
			if(this.cachedMethodArguments == null){
				return null;
			}
			Object[] arguments = new Object[this.cachedMethodArguments.length];
			for(int i = 0; i < arguments.length;i++){
				arguments[i] = resolvedCachedArgument(beanName, this.cachedMethodArguments[i]);
			}
			return arguments;
		}
	}
	
}